{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 3,
   "source": [
    "#  注: jupyter notebook 运行这段后会显示每个中间变量的输出, 不然默认只显示最后一个变量的输出或者不显示.\r\n",
    "from IPython.core.interactiveshell import InteractiveShell\r\n",
    "InteractiveShell.ast_node_interactivity = \"all\"  "
   ],
   "outputs": [],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "# 11 FLASK中的Form表单处理\r\n",
    "\r\n",
    "表单是任何 web 应用程序的重要组成部分，但不幸的是，使用它们是相当困难的。这一切都始于客户机，首先，您必须在客户机端验证数据，然后在服务器上验证。如果这还不够，那么您需要考虑所有的安全问题，比如 CSRF、 XSS、 SQL 注入等等。总而言之，这是一个很大的工作量。幸运的是，我们有一个很好的名为 WTForms 的库，可以帮助我们完成繁重的工作。在我们学习更多关于 WTForms 的知识之前，接下来的部分将为您介绍如何在 Flask 中处理表单而不使用任何库或包。\r\n",
    "\r\n",
    "## 表单处理-艰难之路\r\n",
    "\r\n",
    "使用以下代码创建一个名为 login 的新模板: flask_app/template/login.html\r\n",
    "\r\n",
    "```\r\n",
    "<!DOCTYPE html>\r\n",
    "<html lang=\"en\">\r\n",
    "<head>\r\n",
    "    <meta charset=\"UTF-8\">\r\n",
    "    <title>Login</title>\r\n",
    "</head>\r\n",
    "<body>\r\n",
    "\r\n",
    "    {% if message %}\r\n",
    "        <p>{{ message }}</p>\r\n",
    "    {% endif %}\r\n",
    "\r\n",
    "    <form action=\"\" method=\"post\">\r\n",
    "        <p>\r\n",
    "            <label for=\"username\">Username</label>\r\n",
    "            <input type=\"text\" name=\"username\">\r\n",
    "        </p>\r\n",
    "        <p>\r\n",
    "            <label for=\"password\">Password</label>\r\n",
    "            <input type=\"password\" name=\"password\">\r\n",
    "        </p>\r\n",
    "        <p>\r\n",
    "            <input type=\"submit\">\r\n",
    "        </p>\r\n",
    "    </form>\r\n",
    "\r\n",
    "</body>\r\n",
    "</html>\r\n",
    "```\r\n",
    "\r\n",
    "接下来，在 main2.py 中的 books ()视图函数之后添加以下代码。\r\n",
    "\r\n",
    "flask_app/main2.py\r\n",
    "\r\n",
    "```python\r\n",
    "from flask import Flask, render_template, request\r\n",
    "#...\r\n",
    "@app.route('/login/', methods=['post', 'get'])\r\n",
    "def login():\r\n",
    "    message = ''\r\n",
    "    if request.method == 'POST':\r\n",
    "        username = request.form.get('username')  # access the data inside \r\n",
    "        password = request.form.get('password')\r\n",
    "\r\n",
    "        if username == 'root' and password == 'pass':\r\n",
    "            message = \"Correct username and password\"\r\n",
    "        else:\r\n",
    "            message = \"Wrong username or password\"\r\n",
    "\r\n",
    "    return render_template('login.html', message=message)\r\n",
    "#...\r\n",
    "```\r\n",
    "\r\n",
    "注意传递给 route () 装饰器 的 `methods` 参数。默认情况下，只有当 `request.method` 为 GET 或 HEAD 时才调用请求处理程序。这可以通过向 `methods` 关键字参数传递允许的 HTTP 方法的列表来改变。从现在开始，只有在使用 GET、 POST 或 HEAD 方法请求/login/时，才会调用 login() 视图函数。尝试使用任何其他方法访问 `/login/` 将导致 HTTP 405 -- 此方式不允许的错误。\r\n",
    "\r\n",
    "在前面的课程中，我们已经讨论过 `request` 请求对象提供当前 web 请求的信息。通过表单提交的数据存储在 `request` 请求对象的表单 `form` 属性中。Form 是一个类似于不可变物件的字典，被称为 `ImmutableMultiDict` 。\r\n",
    "\r\n",
    "启动服务器并访问   http://localhost:5000/login/ ，你应该会看到这样的表单。\r\n",
    "\r\n",
    "![img](login_page-bd3d7b96-d2be-4a59-9d89-3be7206d4d75.png)\r\n",
    "\r\n",
    "该页面是使用 GET 请求请求的，因此忽略了 login() 视图函数中 if 块中的代码。\r\n",
    "\r\n",
    "提交表单时不需要输入任何内容，你应该会看到这样的页面:\r\n",
    "\r\n",
    "![img](login_form_error_message-127ed6bf-3ad4-4cbe-8bb9-5c8cd830b65e.png)\r\n",
    "\r\n",
    "这次页面是使用 POST 方法提交的，因此执行 if 块中的代码。在 if 语句 中，我们访问表单提交的用户名和密码，并相应地设置 `message` 消息变量的值。因为我们提交了一个空表单，所以会显示一个错误消息。\r\n",
    "\r\n",
    "用正确的用户名和密码填写表格，然后按回车键。你应该收到以下“正确的用户名和密码”信息:\r\n",
    "\r\n",
    "![img](login_form_success_message-71ec07c2-ec4c-4a6d-b0dc-bc6f137b008e.png)\r\n",
    "\r\n",
    "这就是我们在 Flask 中处理表单的方式，现在让我们把注意力转移到 WTForms 包上。\r\n",
    "\r\n",
    "\r\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "## WTForms\r\n",
    "\r\n",
    "WTForms 是一个用 Python 编写的强大的框架无关(框架独立)库。它允许我们生成 HTML 表单、验证表单、用数据预填充表单(用于编辑)等等。除此之外，它还提供 CSRF 保护。要安装 WTForms，我们使用 Flask-WTF。\r\n",
    "\r\n",
    "`Flask-WTF` 是一个 `Flask` 扩展 整合了 Flask 与 WTForms。`Flask-WTF` 还提供了一些额外的特性，如文件上传、 reCAPTCHA、国际化(i18n)等等。要安装 Flask-WTF，请输入以下命令。\r\n",
    "\r\n",
    "`pip install flask-wtf`\r\n",
    "\r\n",
    "## Creating Form class 创建表单类\r\n",
    "\r\n",
    "我们首先将表单定义为 Python 类。每个表单类都必须扩展来自flask_wtf  包的 `FlaskForm` 类。`FlaskForm` 是一个包装器，其中包含围绕原始 wtform 建立的一些有用的方法。 `FlaskForm` 表单类，它是创建表单的基类。在表单类中，我们将表单字段定义为类变量。表单字段是通过创建与字段类型关联的对象来定义的。Wtform 包提供了几个类，它们代表表单字段，如 `StringField`, `PasswordField`, `SelectField`, `TextAreaField`, `SubmitField` 等。\r\n",
    "\r\n",
    "创建一个新文件 forms.py 在 `flask_app` 目录下 ，并向其中添加以下代码。\r\n",
    "\r\n",
    "```python\r\n",
    "from flask_wtf import FlaskForm\r\n",
    "from wtforms import StringField, SubmitField, TextAreaField\r\n",
    "from wtforms.validators import DataRequired, Email\r\n",
    "\r\n",
    "class ContactForm(FlaskForm):\r\n",
    "    name = StringField(\"Name: \", validators=[DataRequired()])\r\n",
    "    email = StringField(\"Email: \", validators=[Email()])\r\n",
    "    message = TextAreaField(\"Message\", validators=[DataRequired()])\r\n",
    "    submit = SubmitField(\"Submit\")\r\n",
    "\r\n",
    "```\r\n",
    "\r\n",
    "这里我们定义了一个表单类 `ContactForm` ，它包含四个表单字段: `name、 email、 message 和 submit`。这些变量将用于呈现表单字段以及设置和检索字段之间的数据。表单是使用两个 `StringField` ，一个 `TextAreaField` 和一个 `SubmitField` 创建的。每次我们创建一个字段对象时，我们都会向它的构造函数传递一些参数。\r\n",
    "\r\n",
    "第一个参数是包含`标签`的字符串，当表单字段呈现时，标签将显示在 `<label>` 标签中。\r\n",
    "\r\n",
    "第二个可选参数是作为关键字参数传递给构造函数的验证器列表。验证器是决定字段中的数据是否有效的函数或类。我们可以通过用逗号(,)将多个验证器分隔开来将它们应用到一个字段。`wtforms.validators` 验证器模块提供了一些基本的验证器，但我们也可以创建自己的验证器。在这种形式中，我们使用两个内置的验证器 `DataRequired` 和 `Email`。\r\n",
    "\r\n",
    "`DataRequired` : 它确保用户必须在字段中输入一些数据。\r\n",
    "`Email`:  它检查输入的数据是否是有效的电子邮件地址。\r\n",
    "\r\n",
    "注意: WTForm版本从2.2.1开始修改了EMAIL的验证方式, 代码不需要改, 但需要额外安装一个库, \r\n",
    "` pip install wtforms[email] `\r\n",
    "\r\n",
    "<!-- Successfully installed dnspython-2.1.0 email-validator-1.1.3 -->\r\n",
    "<!-- Validates an email address. Requires email_validator package to be installed.  -->\r\n",
    "\r\n",
    "直到所有应用于它的验证器都得到满足,该字段中的数据才会被接受，\r\n",
    "\r\n",
    "注意: 我们仅仅触及了表单字段和验证器的表面，要查看完整的列表，请访问 \r\n",
    "\r\n",
    "https://wtforms.readthedocs.io/en/master/。\r\n",
    "\r\n",
    "表单字段:\r\n",
    "https://wtforms.readthedocs.io/en/master/fields/#basic-fields\r\n",
    "\r\n",
    "验证器:\r\n",
    "https://wtforms.readthedocs.io/en/master/validators/#built-in-validators\r\n",
    "\r\n",
    "小部件:\r\n",
    "https://wtforms.readthedocs.io/en/master/widgets/#built-in-widgets\r\n",
    "\r\n",
    "小部件是用于将字段呈现为其可用表示形式(通常是 XHTML)的类。当调用字段时，默认行为是将呈现委托给它的小部件。提供这种抽象是为了方便地创建小部件以定制现有字段的呈现。\r\n",
    "\r\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "## 设置密钥\r\n",
    "\r\n",
    "默认情况下，Flask-WTF 防止所有形式的 CSRF 攻击(跨站请求伪造)。它通过在表单内部的隐藏 < input > 元素中嵌入一个令牌来实现这一点。然后使用令牌来验证请求的真实性。在 Flask-WTF 生成 csrf 令牌之前，我们必须添加一个密钥。打开 main2.py 并设置密钥如下:\r\n",
    "\r\n",
    "```\r\n",
    "#...\r\n",
    "app.debug = True\r\n",
    "app.config['SECRET_KEY'] = 'a really really really really long secret key'\r\n",
    "\r\n",
    "manager = Manager(app)\r\n",
    "#...\r\n",
    "```\r\n",
    "这里我们使用了 Flask 对象的 `config` 属性。Config 属性的工作原理就像一个字典，它用来放置 Flask 和 Flask 扩展的配置选项，但是如果你愿意，你也可以放置你自己的配置。\r\n",
    "\r\n",
    "密钥应该是一个长的，难以猜测的字符串。SECRET _ key 的使用不仅限于创建 CSRF 令牌，它还被 Flask 和许多其他扩展使用。这个密钥应该保密。与在应用程序中存储密钥相比，更好的方法是将其存储在环境变量文件中。我们将在后面的章节中学习如何做到这一点。\r\n",
    "\r\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "## 控制台中的表单\r\n",
    "\r\n",
    "输入以下命令打开 Python shell: `python main2.py shell`\r\n",
    "\r\n",
    "这将在应用程序上下文中启动 Python shell。\r\n",
    "\r\n",
    "现在(从我们刚编写的 forms.py中) 导入 `ContactForm` 类，并通过向其传递表单数据来实例化一个新的表单对象。(操作记录在本节后面)\r\n",
    "\r\n",
    "注意，我们将表单数据作为 `MultiDict` 对象传递，因为 wtforms 的构造函数 `wtforms.Form` 类接受 `MultiDict` 类型的参数。\r\n",
    "\r\n",
    "如果在实例化表单对象时没有指定表单数据，并且表单是使用 POST 请求提交的，那么 `wtforms.Form` 将使用 `request.Form` 属性中的数据。回想一下 `request.form` 返回一个类型为 `ImmutableMultiDict` 的对象，该对象与 `MultiDict` 对象相同，但是它是不可变的。\r\n",
    "\r\n",
    "使用表单对象的 validate() 方法验证表单。`form1.validate()`\r\n",
    "\r\n",
    "我们的表单无法验证，因为在创建表单对象时，我们没有向所需的 `message`  消息字段提供任何数据。我们可以使用表单对象的 `errors` 属性来访问表单错误:`form1.errors`\r\n",
    "\r\n",
    "请注意，除了  `message` 消息字段的错误消息外，输出还包含缺少的 `csrf` 令牌的错误消息。这是因为我们在表单数据中没有使用 `csrf` 令牌的实际 POST 请求。\r\n",
    "\r\n",
    "我们可以通过在实例化表单类时传递 CSRF_enabled = False 来关闭表单上的 `CSRF` 保护:\r\n",
    "\r\n",
    "正如预期的那样，现在我们只得到丢失的消息字段的错误。让我们创建另一个表单对象，但是这次我们将为所有表单字段提供有效的数据。\r\n",
    "\r\n",
    "这次表单验证成功. \r\n",
    "\r\n",
    "```python\r\n",
    "# 这将在应用程序上下文中启动 Python shell。\r\n",
    "PS D:\\python_blard\\FLASK\\flask_project\\笔记\\flask_app-11a> python main2.py shell \r\n",
    "\r\n",
    "In [1]: from forms import ContactForm #  从我们刚编写的 forms.py 中导入 `ContactForm` 类，并通过向其传递表单数据来实例化一个新的表单对象。\r\n",
    "   ...: from werkzeug.datastructures import MultiDict # 详见下节的补充内容\r\n",
    "   ...:\r\n",
    "   ...: form1 = ContactForm(MultiDict([('name', 'jerry'),('email', 'jerry@mail.com')]))  # 向其传递表单数据来实例化一个新的表单对象\r\n",
    "\r\n",
    "In [2]: form1\r\n",
    "Out[2]: <forms.ContactForm at 0x1d49c00de80>\r\n",
    "\r\n",
    "In [3]: form1.validate() # 使用表单对象的 validate() 方法验证表单\r\n",
    "Out[3]: False\r\n",
    "\r\n",
    "In [4]: form1.errors # 使用表单对象的 `errors` 属性来访问表单错误\r\n",
    "Out[4]: \r\n",
    "{'message': ['This field is required.'],\r\n",
    " 'csrf_token': ['The CSRF token is missing.']}\r\n",
    "\r\n",
    "In [5]: form3 = ContactForm(MultiDict([('name', 'spike'),('email', 'spike@mail.com')]), csrf_enabled=False ) # 关闭表单上的 `CSRF` 保护\r\n",
    "<ipython-input-5-fdb2c1f9782c>:1: FlaskWTFDeprecationWarning: \"csrf_enabled\" is deprecated and will be removed in 1.0. Pass meta={'csrf': False} instead.\r\n",
    "  form3 = ContactForm(MultiDict([('name', 'spike'),('email', 'spike@mail.com')]), csrf_enabled=False)      \r\n",
    "\r\n",
    "In [6]: form3.validate()\r\n",
    "Out[6]: False\r\n",
    "\r\n",
    "In [7]: form3.errors\r\n",
    "Out[7]: {'message': ['This field is required.']}\r\n",
    "\r\n",
    "In [8]: form4 = ContactForm(MultiDict([('name', 'jerry'), ('email', 'jerry@mail.com'), ('message', \"hello tom\")]), csrf_enabled=False) \r\n",
    "<ipython-input-8-a033632c74bf>:1: FlaskWTFDeprecationWarning: \"csrf_enabled\" is deprecated and will be removed in 1.0. Pass meta={'csrf': False} instead.\r\n",
    "  form4 = ContactForm(MultiDict([('name', 'jerry'), ('email', 'jerry@mail.com'), ('message', \"hello tom\")]), csrf_enabled=False)\r\n",
    "\r\n",
    "In [9]: form4.validate() # 这次表单验证成功.\r\n",
    "Out[9]: True\r\n",
    "\r\n",
    "In [10]: form4.errors\r\n",
    "Out[10]: {}\r\n",
    "```\r\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "\r\n",
    "## 以下为Werkzeug的补充内容\r\n",
    "### 关于Werkzeug\r\n",
    "\r\n",
    "Werkzeug 是一个综合性的 WSGI web 应用程序库。它最初是一个用于 WSGI 应用程序的各种实用程序的简单集合，现在已经成为最先进的 WSGI 实用程序库之一。\r\n",
    "\r\n",
    "Werkzeug Tutorial 教程  https://werkzeug.palletsprojects.com/en/1.0.x/tutorial\r\n",
    "\r\n",
    "Werkzeug 内部制定了一些复杂的数据结构, 比如: MultiDict ImmutableMultiDict \r\n",
    "\r\n",
    "### MultiDict \r\n",
    "\r\n",
    "MultiDict 是一个自定义的字典子类，用于处理同一个键值 `key`含有多个值 `values` 的数据类型，例如， `wrappers`包装器中的解析函数使用的 `key` 。这是必要的，因为一些 HTML 表单元素为同一个键传递多个值。\r\n",
    "\r\n",
    "MultiDict 实现了所有标准的字典方法。在内部，它将键的所有值保存为列表，但标准 dict 访问方法只返回键的第一个值。如果您还想访问其他值，则必须使用下面解释的列表方法。\r\n",
    "\r\n",
    "https://werkzeug.palletsprojects.com/en/1.0.x/datastructures/#werkzeug.datastructures.MultiDict"
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "source": [
    "from werkzeug.datastructures import MultiDict\r\n",
    "\r\n",
    "d = MultiDict([('a', 'b'), ('a', 'c')])\r\n",
    "d\r\n",
    "d['a']\r\n",
    "d.getlist('a')\r\n",
    "'a' in d"
   ],
   "outputs": [
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": [
       "MultiDict([('a', 'b'), ('a', 'c')])"
      ]
     },
     "metadata": {},
     "execution_count": 5
    },
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": [
       "'b'"
      ]
     },
     "metadata": {},
     "execution_count": 5
    },
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": [
       "['b', 'c']"
      ]
     },
     "metadata": {},
     "execution_count": 5
    },
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "metadata": {},
     "execution_count": 5
    }
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "source": [
    "d2  = MultiDict({\"foo\": [1, 2, 3], \"bar\": [4,5,6]})\r\n",
    "d2 \r\n",
    "for key in d2.keys():\r\n",
    "    print(f'{key},{d2.getlist(key)}')\r\n",
    "\r\n",
    "for values in d2.listvalues():\r\n",
    "    print(f'{values}')"
   ],
   "outputs": [
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": [
       "MultiDict([('foo', 1), ('foo', 2), ('foo', 3), ('bar', 4), ('bar', 5), ('bar', 6)])"
      ]
     },
     "metadata": {},
     "execution_count": 9
    },
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "foo,[1, 2, 3]\n",
      "bar,[4, 5, 6]\n",
      "[1, 2, 3]\n",
      "[4, 5, 6]\n"
     ]
    }
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "### ImmutableMultiDict\r\n",
    "一个不可变的 MultiDict。\r\n",
    "https://werkzeug.palletsprojects.com/en/1.0.x/datastructures/?highlight=immutablemultidict\r\n",
    "\r\n",
    "### 补充结束"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "## Rendering Form 渲染表格\r\n",
    "\r\n",
    "有两种方式来渲染/呈现表单字段:\r\n",
    "\r\n",
    "1. 逐个渲染字段\r\n",
    "2. 使用 for 循环渲染字段\r\n",
    "\r\n",
    "###  Rendering fields one by one 逐个渲染字段\r\n",
    "\r\n",
    "在模板 `templates` 中，一旦我们可以访问表单实例，我们就可以使用字段名 `field names` 来呈现字段、标签和错误，如下所示:\r\n",
    "\r\n",
    "```html\r\n",
    "{# render the label tag associated with field #}\r\n",
    "{{ form.field_name.label()  }}  \r\n",
    "\r\n",
    "{# render the field itself #}\r\n",
    "{{ form.field_name()  }}  \r\n",
    "\r\n",
    "{# render the validation errors associated with the field #}\r\n",
    "{% for error in form.field_name.errors %}\r\n",
    "    {{ error }}  \r\n",
    "{% endfor %}\r\n",
    "```\r\n",
    "\r\n",
    "让我们在控制台中测试一下(python main2.py shell):\r\n",
    "\r\n",
    "\r\n",
    "\r\n",
    "```python\r\n",
    "PS D:\\python_blard\\FLASK\\flask_project\\笔记\\flask_app-11a> python main2.py shell\r\n",
    "\r\n",
    "In [1]: from forms import ContactForm\r\n",
    "\r\n",
    "In [2]: from jinja2 import Template\r\n",
    "\r\n",
    "In [3]: form = ContactForm()  # 在这里，我们实例化了表单对象，没有任何请求数据，这通常是第一次使用 GET 请求显示表单的情况。\r\n",
    "\r\n",
    "In [4]: Template(\"{{ form.name.label() }}\").render(form=form)\r\n",
    "Out[4]: '<label for=\"name\">Name: </label>'\r\n",
    "\r\n",
    "In [5]: Template(\"{{ form.name() }}\").render(form=form)\r\n",
    "Out[5]: '<input id=\"name\" name=\"name\" required type=\"text\" value=\"\">'\r\n",
    "\r\n",
    "In [6]: Template(\"{{ form.email.label() }}\").render(form=form)\r\n",
    "Out[6]: '<label for=\"email\">Email: </label>'\r\n",
    "\r\n",
    "In [7]: Template(\"{{ form.email() }}\").render(form=form)\r\n",
    "Out[7]: '<input id=\"email\" name=\"email\" type=\"text\" value=\"\">'\r\n",
    "\r\n",
    "In [8]: Template(\"{{ form.message.label() }}\").render(form=form)\r\n",
    "Out[8]: '<label for=\"message\">Message</label>'\r\n",
    "\r\n",
    "In [9]: Template(\"{{ form.message() }}\").render(form=form)\r\n",
    "Out[9]: '<textarea id=\"message\" name=\"message\" required>\\r\\n</textarea>'\r\n",
    "\r\n",
    "In [10]: Template(\"{{ form.submit() }}\").render(form=form)\r\n",
    "Out[10]: '<input id=\"submit\" name=\"submit\" type=\"submit\" value=\"Submit\">'\r\n",
    "\r\n",
    "# 由于窗体是第一次显示，所以它的字段都不会有任何验证错误。下面的代码演示了这一点:\r\n",
    "\r\n",
    "In [11]: Template(\"{% for error in form.name.errors %}{{ error }}{% endfor %}\").render(form=form)\r\n",
    "Out[11]: ''\r\n",
    "\r\n",
    "In [12]: Template(\"{% for error in form.email.errors %}{{ error }}{% endfor %}\").render(form=form)\r\n",
    "Out[12]: ''\r\n",
    "\r\n",
    "In [13]: Template(\"{% for error in form.message.errors %}{{ error }}{% endfor %}\").render(form=form)\r\n",
    "Out[13]: ''\r\n",
    "\r\n",
    "# 您可以使用  `form.errors` 访问与表单相关联的所有验证错误，而不是按字段显示验证错误。 `forms.errors` 通常用于在窗体顶部显示验证错误。\r\n",
    "\r\n",
    "In [14]: Template(\"{% for error in form.errors %}{{ error }}{% endfor %}\").render(form=form)\r\n",
    "Out[14]: ''\r\n",
    "\r\n",
    "# 在呈现字段和标签时，我们还可以提供额外的关键字参数，这些参数将作为键值对注入到 HTML 中。例如:\r\n",
    "\r\n",
    "In [15]: Template('{{ form.name(class=\"input\", id=\"simple-input\") }}').render(form=form)\r\n",
    "Out[15]: '<input class=\"input\" id=\"simple-input\" name=\"name\" required type=\"text\" value=\"\">'\r\n",
    "\r\n",
    "In [16]: Template('{{ form.name.label(class=\"lbl\") }}').render(form=form)\r\n",
    "Out[16]: '<label class=\"lbl\" for=\"name\">Name: </label>'\r\n",
    "\r\n",
    "```\r\n",
    "\r\n",
    "\r\n",
    "现在假设我们的表单已经提交，让我们尝试渲染字段，看看会发生什么。\r\n",
    "\r\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "```python\r\n",
    "PS D:\\python_blard\\FLASK\\flask_project\\笔记\\flask_app-11a> python main2.py shell\r\n",
    "\r\n",
    "In [1]: from werkzeug.datastructures import MultiDict\r\n",
    "\r\n",
    "In [4]: from forms import ContactForm\r\n",
    "\r\n",
    "In [5]: form = ContactForm(MultiDict([('name', 'spike'),('email', 'spike@mail.com')]))\r\n",
    "\r\n",
    "\r\n",
    "\r\n",
    "In [6]: form.validate()\r\n",
    "Out[6]: False\r\n",
    "\r\n",
    "In [8]: from jinja2 import Template\r\n",
    "\r\n",
    "In [9]: Template(\"{{ form.name() }}\").render(form=form)\r\n",
    "Out[9]: '<input id=\"name\" name=\"name\" required type=\"text\" value=\"spike\">'\r\n",
    "\r\n",
    "In [10]: Template(\"{{ form.email() }}\").render(form=form)\r\n",
    "Out[10]: '<input id=\"email\" name=\"email\" type=\"text\" value=\"spike@mail.com\">'\r\n",
    "\r\n",
    "In [11]: Template(\"{{ form.message() }}\").render(form=form)\r\n",
    "Out[11]: '<textarea id=\"message\" name=\"message\" required>\\r\\n</textarea>'\r\n",
    "\r\n",
    "# 注意，name 和 email 字段的 value 属性是用数据填充的。但是，message 消息字段的 <textarea> 元素是空的，因为我们没有向它提供任何数据。我们可以通过以下方式访问消息字段的验证错误:\r\n",
    "\r\n",
    "In [12]: Template(\"{% for error in form.message.errors %}{{ error }}{% endfor %}\").render(form=form)\r\n",
    "    ...: 'This field is required.'\r\n",
    "Out[12]: 'This field is required.'\r\n",
    "\r\n",
    "# 或者，也可以使用 form.errors 一次循环遍历所有验证错误。\r\n",
    "\r\n",
    "In [13]: s =\"\"\"\\\r\n",
    "    ...: ... {% for field_name in form.errors %}\\\r\n",
    "    ...: ...         {% for error in form.errors[field_name] %}\\\r\n",
    "    ...: ...             <li>{{ field_name }}: {{ error }}</li>\r\n",
    "    ...: ...         {% endfor %}\\\r\n",
    "    ...: ... {% endfor %}\\\r\n",
    "    ...: ... \"\"\"\r\n",
    "\r\n",
    "In [14]: Template(s).render(form=form)\r\n",
    "Out[14]: '                    <li>message: This field is required.</li>\\n                            <li>csrf_token: The CSRF token is missing.</li>\\n   \r\n",
    "     '\r\n",
    "\r\n",
    "# 请注意，我们正在得到缺少 csrf 令牌错误，因为请求是在没有 csrf 令牌的情况下提交的。我们可以像普通字段一样渲染 csrf 字段，如下:\r\n",
    "\r\n",
    "In [15]: Template(\"{{ form.csrf_token() }}\").render(form=form)\r\n",
    "Out[15]: '<input id=\"csrf_token\" name=\"csrf_token\" type=\"hidden\" value=\"Ijg0NGRkMTE4MTA3MTdhYWUwYzZiYzE3NDc2YzkyYWJlYWMyNmFkZmMi.YSc-Sg.3wGlu55YlTmu1HDyuetqIQc6_yA\">'\r\n",
    "\r\n",
    "# 如果您有相当多的表单字段，逐个呈现字段可能会非常麻烦。对于这种情况，可以使用 For 循环来呈现字段。\r\n",
    "```\r\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "\r\n",
    "## 使用循环渲染字段\r\n",
    "\r\n",
    "```python\r\n",
    "In [16]: s = \"\"\"\\\r\n",
    "    ...: ...     <div>\r\n",
    "    ...: ...         {{ form.csrf_token }}\r\n",
    "    ...: ...     </div>\r\n",
    "    ...: ... {% for field in form if field.name != 'csrf_token' %}\r\n",
    "    ...: ...     <div>\r\n",
    "    ...: ...         {{ field.label() }}\r\n",
    "    ...: ...         {{ field() }}\r\n",
    "    ...: ...         {% for error in field.errors %}\r\n",
    "    ...: ...             <div class=\"error\">{{ error }}</div>\r\n",
    "    ...: ...         {% endfor %}\r\n",
    "    ...: ...     </div>\r\n",
    "    ...: ... {% endfor %}\r\n",
    "    ...: ... \"\"\"\r\n",
    "\r\n",
    "In [17]: Template(s).render(form=form)\r\n",
    "Out[17]: '    <div>\\n        <input id=\"csrf_token\" name=\"csrf_token\" type=\"hidden\" value=\"Ijg0NGRkMTE4MTA3MTdhYWUwYzZiYzE3NDc2YzkyYWJlYWMyNmFkZmMi.YSc-Sg.3wGlu55YlTmu1HDyuetqIQc6_yA\">\\n    </div>\\n\\n    <div>\\n        <label for=\"name\">Name: </label>\\n        <input id=\"name\" name=\"name\" required type=\"text\" value=\"spike\">\\n        \\n    </div>\\n\\n    <div>\\n        <label for=\"email\">Email: </label>\\n        <input id=\"email\" name=\"email\" type=\"text\" value=\"spike@mail.com\">\\n        \\n    </div>\\n\\n    <div>\\n        <label for=\"message\">Message</label>\\n        <textarea id=\"message\" name=\"message\" required>\\r\\n</textarea>\\n        \\n            <div class=\"error\">This field is required.</div>\\n        \\n    </div>\\n\\n    <div>\\n        <label for=\"submit\">Submit</label>\\n        <input id=\"submit\" name=\"submit\" type=\"submit\" value=\"Submit\">\\n        \\n    </div>\\n'\r\n",
    "\r\n",
    "In [18]: print(Template(s).render(form=form))\r\n",
    "    <div>\r\n",
    "        <input id=\"csrf_token\" name=\"csrf_token\" type=\"hidden\" value=\"Ijg0NGRkMTE4MTA3MTdhYWUwYzZiYzE3NDc2YzkyYWJlYWMyNmFkZmMi.YSc-Sg.3wGlu55YlTmu1HDyuetqIQc6_yA\">\r\n",
    "    </div>\r\n",
    "\r\n",
    "    <div>\r\n",
    "        <label for=\"name\">Name: </label>\r\n",
    "        <input id=\"name\" name=\"name\" required type=\"text\" value=\"spike\">\r\n",
    "\r\n",
    "    </div>\r\n",
    "\r\n",
    "    <div>\r\n",
    "        <label for=\"email\">Email: </label>\r\n",
    "        <input id=\"email\" name=\"email\" type=\"text\" value=\"spike@mail.com\">\r\n",
    "\r\n",
    "    </div>\r\n",
    "\r\n",
    "    <div>\r\n",
    "        <label for=\"message\">Message</label>\r\n",
    "        <textarea id=\"message\" name=\"message\" required>\r\n",
    "</textarea>\r\n",
    "\r\n",
    "            <div class=\"error\">This field is required.</div>\r\n",
    "\r\n",
    "    </div>\r\n",
    "\r\n",
    "    <div>\r\n",
    "        <label for=\"submit\">Submit</label>\r\n",
    "        <input id=\"submit\" name=\"submit\" type=\"submit\" value=\"Submit\">\r\n",
    "\r\n",
    "    </div>\r\n",
    "\r\n",
    "\r\n",
    "```\r\n",
    "\r\n",
    "** 需要注意的是，无论使用哪种方法，都必须手动添加 <form> 标记来包装表单字段。**\r\n",
    "\r\n",
    "\r\n",
    "现在我们知道了如何创建、验证和呈现表单。让我们用这些知识来创建一些真实的表单。\r\n",
    "\r\n",
    "首先用下面的代码创建一个新的模板 `contact.html` :\r\n",
    "\r\n",
    "```html\r\n",
    "<!DOCTYPE html>\r\n",
    "<html lang=\"en\">\r\n",
    "<head>\r\n",
    "    <meta charset=\"UTF-8\">\r\n",
    "    <title>Title</title>\r\n",
    "</head>\r\n",
    "<body>\r\n",
    "\r\n",
    "<form action=\"\" method=\"post\">\r\n",
    "\r\n",
    "    {{ form.csrf_token() }}\r\n",
    "\r\n",
    "    {% for field in form if field.name != \"csrf_token\" %}\r\n",
    "        <p>{{ field.label() }}</p>\r\n",
    "        <p>{{ field }}\r\n",
    "            {% for error in field.errors %}\r\n",
    "                {{ error }}\r\n",
    "            {% endfor %}\r\n",
    "        </p>\r\n",
    "    {% endfor %}\r\n",
    "\r\n",
    "</form>\r\n",
    "\r\n",
    "</body>\r\n",
    "</html>\r\n",
    "```\r\n",
    "\r\n",
    "唯一缺少的一块拼图是视图功能，我们将在下一步创建。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "## 处理递交表格\r\n",
    "\r\n",
    "打开 main2.py 并在 login ()视图函数之后添加以下代码\r\n",
    "\r\n",
    "```python\r\n",
    "from flask import Flask, render_template, request, redirect, url_for\r\n",
    "from flask_script import Manager, Command, Shell\r\n",
    "from forms import ContactForm # 加载ContactForm类\r\n",
    "#...\r\n",
    "@app.route('/contact/', methods=['get', 'post'])\r\n",
    "def contact():\r\n",
    "    form = ContactForm()  # 第7行 创建一个叫 `form` 的对象实体\r\n",
    "                          # 在实例化表单对象时，我们没有传递任何数据，\r\n",
    "                          # 因为当使用 `POST` 请求 `WTForms` 提交表单时，将从 `request.form` 属性中读取表单的数据\r\n",
    "    if form.validate_on_submit():  # 检查 `validate_on_submit()` 方法的返回值. validate_on_submit 只在 methods='post' 的时候生效, 'get' 时为false.\r\n",
    "        name = form.name.data\r\n",
    "        email = form.email.data\r\n",
    "        message = form.message.data\r\n",
    "        print(name)\r\n",
    "        print(email)\r\n",
    "        print(message)\r\n",
    "        # db logic goes here\r\n",
    "        print(\"\\nData received. Now redirecting ...\")\r\n",
    "        return redirect(url_for('contact'))\r\n",
    "\r\n",
    "    return render_template('contact.html', form=form)\r\n",
    "#...\r\n",
    "```\r\n",
    "\r\n",
    "在第7行中，我们正在创建一个叫 `form` 的对象实体。在第8行中，我们正在检查 `validate_on_submit()` 方法的返回值，以便在 if 语句体内执行一些代码。\r\n",
    "\r\n",
    "为什么不是像在控制台中那样使用  `validate()`  而是使用 `validate_on_submit()` 呢？\r\n",
    "\r\n",
    "Validate()方法只检查表单数据是否有效，它不检查请求是否使用 `POST` 方法提交。这意味着如果使用 `validate()` 方法，那么对 `/contact/` 的 `GET` 请求也将触发表单验证，用户将在表单中看到验证错误。通常，只有在使用 `POST` 请求提交数据时，我们才触发验证。当使用 `POST` 请求提交表单且数据有效时，`validate_on_submit()` 方法返回 `True` , 否则就 `False` 出错了(注: 包括 `get` 请求时,也返回`False`)。  `validate_on_submit()` 方法在内部调用 `validate()` 方法。\r\n",
    "\r\n",
    "另外，请注意，在实例化表单对象时，我们没有传递任何数据，因为当使用 `POST` 请求 `WTForms` 提交表单时，将从 `request.form` 属性中读取表单的数据。\r\n",
    "\r\n",
    "在 表单类 中定义的表单字段 变成了 表单对象的属性。 \r\n",
    "(指Forms.py 中的 `ContactForm` 类的各个定义为 wtforms.fields 的字段  变成了 `form` 的对象实体的属性 )\r\n",
    "\r\n",
    "要访问字段数据，我们使用表单字段的 `data`  属性。\r\n",
    "\r\n",
    "```\r\n",
    "form.name.data    # access the data in the name field.\r\n",
    "form.email.data   # access the data in the email field.\r\n",
    "```\r\n",
    "\r\n",
    "要一次访问所有表单数据，请使用表单对象的数据属性: `form.data`\r\n",
    "\r\n",
    "当您使用 GET 请求访问 `/contact/` 时，validate_on_submit() 方法返回 False，if 语句中的代码被跳过，用户显示为一个空的 HTML 表单。\r\n",
    "\r\n",
    "启动服务器(如果还没有运行的话: `python .\\main2.py runserver` 注意有没有其他已启动的python进程, 建议先关闭它们)，\r\n",
    "\r\n",
    "访问 http://127.0.0.1:5000/contact/ ，你可以看到如下的联系表单:\r\n",
    "(url的路径分大小写, 不能写成 http://127.0.0.1:5000/Contact/ 会无法访问)\r\n",
    "\r\n",
    "![img](contact_form-40dbc56c-d9cd-46bc-9484-9b66df9e9739.png)\r\n",
    "\r\n",
    "没有输入任何命中提交，你将会看到如下的验证错误:\r\n",
    "\r\n",
    "![img](validation_errors_in_contact_form-7ba6605c-2140-417e-9f1e-d2cb986f3067.png)\r\n",
    "\r\n",
    "在“名称和消息”字段中输入一些数据，在“电子邮件”字段中输入无效数据，然后再次提交表单。\r\n",
    "\r\n",
    "![img](pre-populating_field_data_from_the_previous_request-6f22fa84-ad6e-498c-b868-4ce9c18a6c67.png)\r\n",
    "\r\n",
    "请注意，所有字段仍然包含来自前一个请求的数据。\r\n",
    "\r\n",
    "在 Email 字段中输入一个有效的 Email 并点击 submit。这次我们的验证将会成功，在运行服务器的 shell 中，你会看到如下输出:\r\n",
    "\r\n",
    "```\r\n",
    "Spike\r\n",
    "spike@gmail.com\r\n",
    "A Message\r\n",
    "\r\n",
    "Data received. Now redirecting ...\r\n",
    "```\r\n",
    "\r\n",
    "在 shell 中显示提交的数据之后，视图函数将再次将用户重定向到`/contact/`。此时，您应该看到一个没有任何验证错误的空表单，就好像您第一次使用 GET 请求访问 `/contact/` 一样。\r\n",
    "\r\n",
    "在成功提交表单后，向用户显示一些反馈信息是很好的做法。在 Flask 中，我们使用下面将要讨论的 flash 即时消息来创建这样的反馈。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "## 即时消息 Flash Messages\r\n",
    "\r\n",
    "`Flash Messages`即时消息是依赖于密钥的另一项功能。因为场景后面的 flash 消息存储在会话中，所以密钥是必需的。在深入的课程`Session in Flask`中, 我们将学习什么是`sessions`(会话)以及如何使用`sessions`。\r\n",
    "\r\n",
    "既然我们已经设置了密钥，就可以开始了。\r\n",
    "\r\n",
    "为了 `Flash Messages`，我们从 flask 包中引入 flash() 函数。\r\n",
    "\r\n",
    "flash()函数接受两个参数，即要闪存的消息和一个可选的类别。该类别指示消息的类型，如 `success, error, warning` 成功、错误、警告等。可以在模板中使用该类别来确定要显示的消息的类型。\r\n",
    "\r\n",
    "打开 `main2.py` ，在 contact() 视图函数中的 redirect ()调用之前添加:  `flash(\"Message Received\", \"success\")` ，如下所示:\r\n",
    "\r\n",
    "```python\r\n",
    "from flask import Flask, render_template, request, redirect, url_for, flash\r\n",
    "#...\r\n",
    "        # db logic goes here\r\n",
    "        print(\"\\nData received. Now redirecting ...\")\r\n",
    "        flash(\"Message Received\", \"success\")\r\n",
    "        return redirect(url_for('contact'))\r\n",
    "    return render_template('contact.html', form=form)\r\n",
    "```   \r\n",
    "\r\n",
    "Flash()函数设置的消息将只对接下来的  `request` 请求可用，请求一次后它会被删除。\r\n",
    "\r\n",
    "我们现在设置的 即时消息，为了显示它，我们还须修改我们的模板。\r\n",
    "\r\n",
    "flask_app/templates/contact.html\r\n",
    "```html\r\n",
    "<body>\r\n",
    "\r\n",
    "{% for category, message in get_flashed_messages(with_categories=true) %}\r\n",
    "    <p class=\"{{ category }}\">{{ message }}</p>\r\n",
    "{% endfor %}\r\n",
    "\r\n",
    "<form action=\"\" method=\"post\">\r\n",
    "```\r\n",
    "Jinja 提供了一个名为 `get_flashed_messages()` 的函数，它返回一个没有 `消息类别` 的挂起的 `即时消息` 列表。如果要设置 `消息类别`, 在调用 `get_flash_messages()` 时，使用 `with_categories=True`。当 `with_categories=True` 设置为 True 时，`get_flashed_messages()` 返回窗体的元组列表`(category, message)` 。\r\n",
    "\r\n",
    "在做出这些改变之后，请再次访问 http://localhost:5000/contact/。填写表格然后点击提交。这次你应该在表单顶部得到一个成功的信息，如下所示:\r\n",
    "\r\n",
    "![img](flash_message-f7d879e7-b3de-407a-bffc-634cbfd0f79e.png)\r\n"
   ],
   "metadata": {}
  }
 ],
 "metadata": {
  "orig_nbformat": 4,
  "language_info": {
   "name": "python",
   "version": "3.9.6",
   "mimetype": "text/x-python",
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "pygments_lexer": "ipython3",
   "nbconvert_exporter": "python",
   "file_extension": ".py"
  },
  "kernelspec": {
   "name": "python3",
   "display_name": "Python 3.9.6 64-bit"
  },
  "interpreter": {
   "hash": "c644d696b95f5e0f4df3c6556741cf30bcf9ea6ca93c3e1f29fcf31d885534fc"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}